#!/usr/bin/python2
# -*- coding: utf-8 -*-

import re
import sys
import os

from ryu.base import app_manager
from ryu.cmd.manager import main
from ryu.controller import ofp_event
from ryu.controller.handler import MAIN_DISPATCHER, CONFIG_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3, ofproto_v1_3_parser
from ryu.topology import switches
from webob import Request

from lib.topo import Topo
from lib.executor import Executor as Sequential
from lib.executor_merge import Executor as UA

from lib.updategenerator import UpdateGenerator
# from Queue import Queue
from eventlet import Queue

import predef


from ryu.app.wsgi import ControllerBase
from ryu.app.wsgi import Response

from ryu.app.wsgi import route
from ryu.app.wsgi import WSGIApplication
from ryu.base import app_manager
from ryu.lib import dpid as dpid_lib
from ryu.topology.api import get_switch, get_link, get_host


class TestController(ControllerBase):
    def __init__(self, req, link, data, **config):
        super(TestController, self).__init__(req, link, data, **config)
        self.testapp = data['testapp']

    @route('test', '/ua/test', methods=['GET'])
    def test(self, req, **kwargs):
        return "helloworld"


    @route('test', '/ua/data', methods=['POST'])
    def test1(self, req, **kwargs):
        # assert isinstance(req, Request)
        # la = int(req.body[7:])
        # print(req)
        # r = req.json_body()
        # la = r['lambda']
        import json

        return json.dumps(self.testapp.get_last_update_list(10))
        # return "ok"

    @route('test', '/ua/switch', methods=['GET','POST'])
    def test2(self, req, **kwargs):
        # assert isinstance(req, Request)
        self.testapp.switch_executor()
        return "ok"

    @route('test', '/ua/lambda', methods=['POST'])
    def test3(self, req, **kwargs):
        assert isinstance(req, Request)
        la = int(req.body[7:])
        print(req)
        # r = req.json_body()
        # la = r['lambda']
        self.testapp.set_lambda(la*0.1)
        return "ok"

    @route('test', '/test.html', methods=['GET'])
    def index(self, req, **kwargs):
        path = os.path.dirname(os.path.realpath(__file__))+"/static/test.html"
        return open(path).read()


class TestApp(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    _CONTEXTS = {'switches': switches.Switches,
                 'wsgi':WSGIApplication}

    def __init__(self, *args, **kwargs):
        super(TestApp, self).__init__(*args, **kwargs)

        wsgi = kwargs['wsgi']
        wsgi.register(TestController, {'testapp': self})

        self.__switches = kwargs['switches']
        self.__lldp_count = 0
        self.__topo = Topo()
        self.__hostTable_mac2port = {}
        self.__hostTable_ip2port = {}
        # self.__ip1 = "10.0.0.24"  # hn23
        # self.__ip2 = "10.0.0.22" # hn9
        self.__started = False
        self.__notify_queue = Queue()

        self.__hostTable_ip2port = predef.hostTable
        self.__ACLlist = predef.ACLlist

        self.update_time_list = []

    def get_last_update_list(self, num):
        return self.update_time_list[-num:]

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def _switch_features_handler(self, ev):
        datapath = ev.msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        assert parser is ofproto_v1_3_parser
        # self.__request_stats(datapath)
        # install the table-miss flow entry.
        match = parser.OFPMatch()
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        self.__add_flow(datapath, 0, match, actions)
        # if datapath.id == 3:
        #     self.__request_removed_flow_test(datapath)

    # def __request_removed_flow_test(self, dp):
    #     of = ofproto_v1_3
    #     ofp = ofproto_v1_3_parser
    #     Match = ofp.OFPMatch
    #     match = Match(eth_type=0x0800, ipv4_src="10.0.0.1")
    #     req = ofp.OFPFlowStatsRequest(dp, 0,
    #                                   table_id=0,
    #                                   out_port=of.OFPP_ANY,
    #                                   out_group=of.OFPG_ANY,
    #                                   match=match)
    #     print(req)
    #     dp.send_msg(req)

    def __request_stats(self, datapath):
        self.logger.debug('send stats request: %016x', datapath.id)
        parser = datapath.ofproto_parser
        req = parser.OFPFlowStatsRequest(datapath)
        datapath.send_msg(req)

    @set_ev_cls(ofp_event.EventOFPFlowStatsReply, MAIN_DISPATCHER)
    def _flow_stats_reply_handler(self, ev):
        print("========_flow_stats_reply_handler=========")
        msg = ev.msg
        from ryu.ofproto.ofproto_v1_3_parser import OFPFlowStatsReply
        assert isinstance(msg, OFPFlowStatsReply)
        self.__notify_queue.put(msg)
        # body = ev.msg.body
        # for flow in body:
        #     print(flow)

    @set_ev_cls(ofp_event.EventOFPFlowRemoved, MAIN_DISPATCHER)
    def _flow_removed_handler(self, ev):
        print("========_flow_removed_handler=========")
        msg = ev.msg
        from ryu.ofproto.ofproto_v1_3_parser import OFPFlowRemoved
        assert isinstance(msg, OFPFlowRemoved)
        self.__notify_queue.put(msg)

    def __add_flow(self, datapath, priority, match, actions):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        # construct flow_mod message and send it.
        parser = ofproto_v1_3_parser
        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                             actions)]
        mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
                                match=match, instructions=inst)
        datapath.send_msg(mod)

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        from ryu.lib.packet import ethernet, arp
        from ryu.lib import mac

        # print('get pkt')

        msg = ev.msg
        dp = msg.datapath
        ofp = dp.ofproto
        ofp_parser = dp.ofproto_parser
        assert ofp_parser is ofproto_v1_3_parser

        assert isinstance(msg, ofproto_v1_3_parser.OFPPacketIn)
        assert isinstance(msg.match, ofproto_v1_3_parser.OFPMatch)

        pkt, pkt_type, payload = ethernet.ethernet.parser(msg.data)
        assert isinstance(pkt, ethernet.ethernet)

        limit_count = 400 # TODO for test

        if pkt.ethertype == ethernet.ether.ETH_TYPE_LLDP:
            # print('get lldp')
            if self.__lldp_count < limit_count:
                self.__lldp_count += 1
                print("lldp_count=%d"%self.__lldp_count)
        if self.__lldp_count < limit_count:
            return
        elif self.__lldp_count == limit_count:
            self.__topo.build(self.__switches)
            self.__lldp_count += 1

        pkt_in_portno = msg.match.get('in_port')
        if not self.__topo.is_outer_port(dp.id, pkt_in_portno):
            return

        self.__hostTable_mac2port[pkt.src] = (dp.id, pkt_in_portno)

        if pkt.ethertype != ethernet.ether.ETH_TYPE_ARP:
            return
        print('get arp')

        pkt_arp, _, _ = arp.arp.parser(payload)
        assert isinstance(pkt_arp, arp.arp)

        inport = (dp.id, pkt_in_portno)
        if pkt_arp.opcode == arp.ARP_REQUEST:
            self.__hostTable_ip2port[pkt_arp.src_ip] = inport
            # print(self.__hostTable_ip2port)
        elif pkt_arp.opcode == arp.ARP_REPLY:
            self.__hostTable_ip2port[pkt_arp.src_ip] = inport
            # print(self.__hostTable_ip2port)

        # start update if ready
        # if (not self.__started
        #     and self.__ip1 in self.__hostTable_ip2port
        #     and self.__ip2 in self.__hostTable_ip2port):
        #     self.__start_update()
        if not self.__started:
            self.__start_update()

        if pkt.dst == mac.BROADCAST_STR:
            outer_ports = self.__topo.get_outer_ports()
        elif pkt.dst in self.__hostTable_mac2port:
            outer_ports = [self.__hostTable_mac2port[pkt.dst]]
        else:
            return

        # actions = [ofp_parser.OFPActionOutput(ofp.OFPP_FLOOD)]
        # out = ofp_parser.OFPPacketOut(
        #     datapath=dp, buffer_id=msg.buffer_id, in_port=in_port,
        #     actions=actions)
        # dp.send_msg(out)
        if inport in outer_ports:
            outer_ports.remove(inport)
        self.__packet_out(msg.data, outer_ports)

    def __packet_out(self, data, ports):
        ofp_parser = ofproto_v1_3_parser

        ports_map = {}
        for dpid, portno in ports:
            if dpid in ports_map:
                ports_map[dpid].append(portno)
            else:
                ports_map[dpid] = [portno]

        for dpid, port_list in ports_map.items():
            dp = self.__switches.dps.get(dpid)
            assert dp is not None, "switches don't contain dpid"
            actions = [ofp_parser.OFPActionOutput(portno) for portno in port_list]
            out = ofp_parser.OFPPacketOut(datapath=dp,
                                          data=data,
                                          actions=actions,
                                          in_port=ofproto_v1_3.OFPP_CONTROLLER,
                                          buffer_id=ofproto_v1_3.OFP_NO_BUFFER)
            dp.send_msg(out)

    def switch_executor(self):
        if not self.__started:
            return "not initialized"
        for g in self.__gen_list:
            g.switch_queue()

    def set_lambda(self, la):
        if not self.__started:
            return "not initialized"
        for g in self.__gen_list:
            g.set_lambda(la)

    def __start_update(self):
        print("========================start update=========================")
        self.__started = True

        print(self.__hostTable_ip2port)

        test_lambda = 1000
        if 'test_lambda' in self.CONF:
            test_lambda = self.CONF['test_lambda']

        print('test_lambda',test_lambda)

        shared_queue = Queue()
        self.__sequential = Sequential(shared_queue,
                                   self.__notify_queue,
                                   self.__switches.dps,
                                   self.__topo, self.update_time_list)
        shared_queue_ua = Queue()
        self.__ua = UA(shared_queue_ua,
                                   self.__notify_queue,
                                   self.__switches.dps,
                                   self.__topo, self.update_time_list)

        self.__gen_list = []
        for ip1,ip2 in self.__ACLlist:
            self.__gen_list.append(UpdateGenerator(shared_queue,shared_queue_ua,
                                                 ip1,
                                                 ip2,
                                                 self.__hostTable_ip2port[ip1],
                                                 self.__hostTable_ip2port[ip2],
                                                 self.__topo,
                                                 test_lambda))

        def run(t):   #TODO
            t.run()

        from ryu.lib.hub import spawn
        print("spawn start")
        spawn(run,self.__sequential)
        spawn(run,self.__ua)
        for updategenerator in self.__gen_list:
            spawn(run,updategenerator)
        print("spawn end")


#######################################


if __name__ == 'testapp.py':
    pass
    # app_manager.require_app("ryu.topology.switches")
    # app_manager.require_app("ryu.app.gui_topology.gui_topology")

if __name__ == '__main__':
    sys.argv.append(sys.argv[0].split('/')[-1])
    # sys.argv.append('--verbose')
    sys.argv.append('--enable-debugger')
    sys.argv.append('--observe-links')
    sys.argv.append('--user-flags')
    sys.argv.append('testapp_flags.py')
    sys.argv.append('--test_lambda')
    sys.argv.append('.1')
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(main())
